package pers.lys.aigc4chat.model.baidu.auth;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.client.methods.HttpRequestBase;
import pers.hll.aigc4chat.base.exception.BizException;
import pers.hll.aigc4chat.base.util.EasyCollUtil;
import pers.hll.aigc4chat.base.util.StringUtil;
import pers.hll.aigc4chat.base.xml.BaiduConfig;
import pers.lys.aigc4chat.model.baidu.constant.HeaderKey;
import pers.lys.aigc4chat.model.baidu.util.DateUtil;
import pers.lys.aigc4chat.model.baidu.util.HttpUtil;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * The V1 implementation of Signer with the BCE signing protocol.
 * <p>代码实现参考自依赖bce-java-sdk的同名类
 * <p>为什么不直接用这个依赖?
 * <ol>
 *     <li>Checkmarx扫描出的问题太多</li>
 *     <li>有大量的用不到的Samples 建议另开一个Samples项目</li>
 *     <li>冗余代码太多 不方便调试</li>
 *     <li>依赖的包太多 其中 Reload4j 可能会影响 slf4j 从而导致很多不可控的问题</li>
 * </ol>
 * <pre>{@code
 *   <dependency>
 *     <groupId>com.baidubce</groupId>
 *     <artifactId>bce-java-sdk</artifactId>
 *     <version>0.10.306</version>
 *   </dependency>
 * }
 *
 * </pre>
 *
 * @author hll
 * @since 2024/05/06
 */
@Slf4j
@UtilityClass
public class BceV1Signer {

    private static final String BCE_AUTH_VERSION = "bce-auth-v1";

    private static final Charset UTF8 = StandardCharsets.UTF_8;

    /**
     * Default headers to sign with the BCE signing protocol.
     */
    private static final Set<String> DEFAULT_HEADERS_TO_SIGN = Sets.newHashSet();

    static {
        DEFAULT_HEADERS_TO_SIGN.add(HeaderKey.HOST.toLowerCase());
        DEFAULT_HEADERS_TO_SIGN.add(HeaderKey.CONTENT_LENGTH.toLowerCase());
        DEFAULT_HEADERS_TO_SIGN.add(HeaderKey.CONTENT_TYPE.toLowerCase());
        DEFAULT_HEADERS_TO_SIGN.add(HeaderKey.CONTENT_MD5.toLowerCase());
    }

    /**
     * Sign the given request with the given set of credentials. Modifies the passed-in request to apply the signature.
     * <a href="https://cloud.baidu.com/doc/Reference/s/njwvz1yfu">生成认证字符串</a>
     *
     * @param httpRequest     the request to sign.
     * @param baiduConfig the credentials to sign the request with.
     */
    public void sign(HttpRequestBase httpRequest, BaiduConfig baiduConfig) {
        httpRequest.addHeader(HeaderKey.HOST, HttpUtil.generateHostHeader(httpRequest.getURI()));
        Date timestamp = new Date();
        // authString = "bce-auth-v1/{accessKeyId}/{timestamp}/{expirationPeriodInSeconds}"
        String authString = StringUtil.join("/", BCE_AUTH_VERSION, baiduConfig.getAccessKeyId(),
                DateUtil.formatAlternateIso8601Date(timestamp), 1800);
        String signingKey = sha256Hex(baiduConfig.getSecretKey(), authString);
        // Formatting the URL with signing protocol.
        String canonicalUri = getCanonicalUriPath(httpRequest.getURI().getPath());
        // Formatting the query string with signing protocol.
        String canonicalQueryString = HttpUtil.getCanonicalQueryString(
                HttpUtil.parseQueryString(httpRequest.getURI().getRawPath()));
        // Sorted the headers should be signed from the request.
        Map<String, String> headerMap =
                EasyCollUtil.getPairedFieldsMap(httpRequest.getAllHeaders(), Header::getName, Header::getValue);
        SortedMap<String, String> headersToSign = getHeadersToSign(headerMap);
        // Formatting the headers from the request based on signing protocol.
        String canonicalHeader = getCanonicalHeaders(headersToSign);
        String canonicalRequest = StringUtil.join("\n",
                httpRequest.getMethod(), canonicalUri, canonicalQueryString, canonicalHeader);
        String signature = sha256Hex(signingKey, canonicalRequest);
        String authorizationHeader = authString + "/" + "/" + signature;
        headerMap.put(HeaderKey.AUTHORIZATION, authorizationHeader);
    }

    private String getCanonicalUriPath(String path) {
        if (path == null) {
            return "/";
        } else if (path.startsWith("/")) {
            return HttpUtil.normalizePath(path);
        } else {
            return "/" + HttpUtil.normalizePath(path);
        }
    }

    private String getCanonicalHeaders(SortedMap<String, String> headers) {
        if (headers.isEmpty()) {
            return "";
        }
        List<String> headerStrings = Lists.newArrayList();
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (key == null) {
                continue;
            }
            String value = entry.getValue();
            if (value == null) {
                value = "";
            }
            headerStrings.add(HttpUtil.normalize(key.trim().toLowerCase()) + ':' + HttpUtil.normalize(value.trim()));
        }
        Collections.sort(headerStrings);
        return StringUtils.join(headerStrings, "\n");
    }

    /**
     * 获取需要签名的Header
     * <ul>
     *   <li>Host</li>
     *   <li>Content-Length</li>
     *   <li>Content-Type</li>
     *   <li>Content-MD5</li>
     *   <li>All headers starts with "x-bce-"</li>
     * </ul>
     *
     * @param headers 请求头
     * @return 需要签名的Header
     */
    private SortedMap<String, String> getHeadersToSign(Map<String, String> headers) {
        SortedMap<String, String> ret = Maps.newTreeMap();
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.isNotEmpty(entry.getValue())
                    && isDefaultHeaderToSign(key)
                    && !HeaderKey.AUTHORIZATION.equalsIgnoreCase(key)) {
                ret.put(key, entry.getValue());
            }
        }
        return ret;
    }

    /**
     * 判断是否是默认需要签名的Header
     *
     * @param header 请求头的名称
     * @return 是否是默认需要签名的Header
     */
    private boolean isDefaultHeaderToSign(String header) {
        header = header.trim().toLowerCase();
        return header.startsWith(HeaderKey.BCE_PREFIX) || DEFAULT_HEADERS_TO_SIGN.contains(header);
    }

    /**
     * 加密 加密算法 HmacSHA256
     *
     * @param signingKey 密钥
     * @param stringToSign 待加密字符串
     * @return 密文
     */
    private String sha256Hex(String signingKey, String stringToSign) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(signingKey.getBytes(UTF8), "HmacSHA256"));
            return new String(Hex.encodeHex(mac.doFinal(stringToSign.getBytes(UTF8))));
        } catch (Exception e) {
            throw BizException.of("Fail to generate the signature", e);
        }
    }
}